/**
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
* file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
* to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
* applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.apache.camel.component.jsch;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Scanner;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
import org.apache.camel.component.file.GenericFileEndpoint;
import org.apache.camel.component.file.GenericFileOperationFailedException;
import org.apache.camel.component.file.remote.RemoteFile;
import org.apache.camel.component.file.remote.RemoteFileConfiguration;
import org.apache.camel.component.file.remote.RemoteFileOperations;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
/**
* SCP remote file operations
*/
public class ScpOperations implements RemoteFileOperations<ScpFile> {
private static final String DEFAULT_KNOWN_HOSTS = "META-INF/.ssh/known_hosts";
private static final Logger LOG = LoggerFactory.getLogger(ScpOperations.class);
private ScpEndpoint endpoint;
private Session session;
private ChannelExec channel;
@Override
public void setEndpoint(GenericFileEndpoint<ScpFile> endpoint) {
this.endpoint = (ScpEndpoint) endpoint;
}
@Override
public boolean deleteFile(String name) throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'delete' not supported by the scp: protocol");
}
@Override
public boolean existsFile(String name) throws GenericFileOperationFailedException {
return false;
}
@Override
public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'rename' not supported by the scp: protocol");
}
@Override
public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
return true;
}
@SuppressWarnings("unchecked")
@Override
public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
OutputStream outputStream = null;
RemoteFile<ScpFile> remoteFile = exchange.getIn().getBody(RemoteFile.class);
try {
// exec 'scp -f rfile' remotely
String command = "scp -f " + remoteFile.getAbsoluteFilePath();
Channel channel = session.openChannel("exec");
((ChannelExec) channel).setCommand(command);
// get I/O streams for remote scp
OutputStream out = channel.getOutputStream();
InputStream inputStream = channel.getInputStream();
channel.connect();
byte[] buf = new byte[1024];
// send '\0'
buf[0] = 0;
out.write(buf, 0, 1);
out.flush();
while (true) {
int c = checkAck(inputStream);
if (c != 'C') {
break;
}
// read '0644 '
inputStream.read(buf, 0, 5);
long filesize = 0L;
while (true) {
if (inputStream.read(buf, 0, 1) < 0) {
// error
break;
}
if (buf[0] == ' ') {
break;
}
filesize = filesize * 10L + buf[0] - '0';
}
for (int i = 0;; i++) {
inputStream.read(buf, i, 1);
if (buf[i] == (byte) 0x0a) {
break;
}
}
// send '\0'
buf[0] = 0;
out.write(buf, 0, 1);
out.flush();
// read content
outputStream = new ByteArrayOutputStream();
int foo;
while (true) {
if (buf.length < filesize) {
foo = buf.length;
} else {
foo = (int) filesize;
}
foo = inputStream.read(buf, 0, foo);
if (foo < 0) {
// error
break;
}
outputStream.write(buf, 0, foo);
filesize -= foo;
if (filesize == 0L) {
break;
}
}
if (checkAck(inputStream) != 0) {
LOG.warn("Issues while retrieving the file. Will try again in the next poll.");
return false;
}
// send '\0'
buf[0] = 0;
out.write(buf, 0, 1);
out.flush();
exchange.getIn().setBody(outputStream);
return true;
}
} catch (Exception e) {
LOG.warn("Issues while retrieving the file. Will try again in the next poll. Exception: ", e);
}
return false;
}
private static int checkAck(InputStream in) throws IOException {
int b = in.read();
// b may be 0 for success,
// 1 for error,
// 2 for fatal error,
// -1
if (b == 0) {
return b;
}
if (b == -1) {
return b;
}
if (b == 1 || b == 2) {
StringBuffer sb = new StringBuffer();
int c;
do {
c = in.read();
sb.append((char) c);
} while (c != '\n');
if (b == 1) { // error
System.out.print(sb.toString());
}
if (b == 2) { // fatal error
System.out.print(sb.toString());
}
}
return b;
}
@Override
public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException {
OutputStream outputStream = exchange.getIn().getBody(OutputStream.class);
if(outputStream != null) {
try {
outputStream.close();
} catch(Exception e) {
LOG.trace("Exception caught while closing output stream: ", e);
}
}
}
@Override
public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
ObjectHelper.notNull(session, "session");
ScpConfiguration cfg = endpoint.getConfiguration();
int timeout = cfg.getConnectTimeout();
if (LOG.isTraceEnabled()) {
LOG.trace("Opening channel to {} with {} timeout...", cfg.remoteServerInformation(), timeout > 0 ? (Integer.toString(timeout) + " ms") : "no");
}
String file = getRemoteFile(name, cfg);
InputStream is = null;
if (exchange.getIn().getBody() == null) {
// Do an explicit test for a null body and decide what to do
if (endpoint.isAllowNullBody()) {
LOG.trace("Writing empty file.");
is = new ByteArrayInputStream(new byte[] {});
} else {
throw new GenericFileOperationFailedException("Cannot write null body to file: " + name);
}
}
try {
channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(getScpCommand(cfg, file));
channel.connect(timeout);
LOG.trace("Channel connected to {}", cfg.remoteServerInformation());
try {
if (is == null) {
is = exchange.getIn().getMandatoryBody(InputStream.class);
}
write(channel, file, is, cfg);
} catch (InvalidPayloadException e) {
throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
} catch (IOException e) {
throw new GenericFileOperationFailedException("Failed to write file " + file, e);
} finally {
// must close stream after usage
IOHelper.close(is);
}
} catch (JSchException e) {
throw new GenericFileOperationFailedException("Failed to write file " + file, e);
} finally {
if (channel != null) {
LOG.trace("Disconnecting 'exec' scp channel");
channel.disconnect();
channel = null;
LOG.trace("Channel disconnected from {}", cfg.remoteServerInformation());
}
}
return true;
}
@Override
public String getCurrentDirectory() throws GenericFileOperationFailedException {
return endpoint.getConfiguration().getDirectory();
}
@Override
public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'cd " + path + "' not supported by the scp: protocol");
}
@Override
public void changeToParentDirectory() throws GenericFileOperationFailedException {
throw new GenericFileOperationFailedException("Operation 'cd ..' not supported by the scp: protocol");
}
@Override
public List<ScpFile> listFiles() throws GenericFileOperationFailedException {
System.out.println("listFiles");
throw new GenericFileOperationFailedException("Operation 'ls' not supported by the scp: protocol");
}
@Override
public List<ScpFile> listFiles(String path) throws GenericFileOperationFailedException {
List<ScpFile> files = new ArrayList<ScpFile>();
StringBuffer sb = new StringBuffer();
try {
String command = "ls -als " + path;
Channel channel = session.openChannel("exec");
((ChannelExec) channel).setCommand(command);
((ChannelExec) channel).setErrStream(System.err);
InputStream in = channel.getInputStream();
channel.connect();
byte[] tmp = new byte[1024];
while (true) {
while (in.available() > 0) {
int i = in.read(tmp, 0, 1024);
if (i < 0) {
break;
}
sb.append(new String(tmp, 0, i));
}
if (channel.isClosed()) {
if (in.available() > 0) {
continue;
}
break;
}
try {
Thread.sleep(1000);
} catch (Exception ee) {}
}
channel.disconnect();
} catch (Exception e) {
System.err.println(e);
}
Scanner scanner = new Scanner(sb.toString());
if (scanner.hasNextLine()) {
scanner.nextLine();
scanner.nextLine();
scanner.nextLine();
while (scanner.hasNextLine()) {
ScpFile scpFile = getScpFilename(scanner.nextLine(), path);
if (scpFile != null) {
files.add(scpFile);
}
}
}
return files;
}
private ScpFile getScpFilename(String nextLine, String path) {
if (nextLine == null || nextLine.equals("")) {
return null;
}
ScpFile scpFile = new ScpFile();
String[] pieces = nextLine.split(" +");
if (!pieces[1].startsWith("d")) {
int owner = getPermission(pieces[1].substring(1, 4));
int group = getPermission(pieces[1].substring(4, 7));
int other = getPermission(pieces[1].substring(7, 10));
int attr = owner * 100 + group * 10 + other;
scpFile.setAttrs(attr);
scpFile.setName(pieces[9]);
scpFile.setParent(path);
scpFile.setLength(Integer.parseInt(pieces[5]));
scpFile.setDirectory(false);
return scpFile;
} else {
return null;
}
}
private int getPermission(String permission) {
int permissionVal = 0;
if (permission.contains("r")) {
permissionVal += +4;
}
if (permission.contains("w")) {
permissionVal += +2;
}
if (permission.contains("x") || permission.contains("t")) {
permissionVal += +1;
}
return permissionVal;
}
@Override
public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException {
if (!isConnected()) {
session = createSession(configuration instanceof ScpConfiguration ? (ScpConfiguration) configuration : null);
// TODO: deal with reconnection attempts
if (!isConnected()) {
session = null;
throw new GenericFileOperationFailedException("Failed to connect to " + configuration.remoteServerInformation());
}
}
return true;
}
@Override
public boolean isConnected() throws GenericFileOperationFailedException {
return session != null && session.isConnected();
}
@Override
public void disconnect() throws GenericFileOperationFailedException {
if (isConnected()) {
session.disconnect();
}
session = null;
}
@Override
public boolean sendNoop() throws GenericFileOperationFailedException {
return true;
}
@Override
public boolean sendSiteCommand(String command) throws GenericFileOperationFailedException {
return true;
}
private Session createSession(ScpConfiguration config) {
ObjectHelper.notNull(config, "ScpConfiguration");
try {
final JSch jsch = new JSch();
// get from configuration
if (ObjectHelper.isNotEmpty(config.getCiphers())) {
LOG.debug("Using ciphers: {}", config.getCiphers());
Hashtable<String, String> ciphers = new Hashtable<String, String>();
ciphers.put("cipher.s2c", config.getCiphers());
ciphers.put("cipher.c2s", config.getCiphers());
JSch.setConfig(ciphers);
}
if (ObjectHelper.isNotEmpty(config.getPrivateKeyFile())) {
LOG.debug("Using private keyfile: {}", config.getPrivateKeyFile());
String pkfp = config.getPrivateKeyFilePassphrase();
jsch.addIdentity(config.getPrivateKeyFile(), ObjectHelper.isNotEmpty(pkfp) ? pkfp : null);
}
String knownHostsFile = config.getKnownHostsFile();
jsch.setKnownHosts(ObjectHelper.isEmpty(knownHostsFile) ? DEFAULT_KNOWN_HOSTS : knownHostsFile);
session = jsch.getSession(config.getUsername(), config.getHost(), config.getPort());
session.setTimeout(config.getTimeout());
session.setUserInfo(new SessionUserInfo(config));
if (ObjectHelper.isNotEmpty(config.getStrictHostKeyChecking())) {
LOG.debug("Using StrickHostKeyChecking: {}", config.getStrictHostKeyChecking());
session.setConfig("StrictHostKeyChecking", config.getStrictHostKeyChecking());
} else {
LOG.debug("Using StrickHostKeyChecking: {}", "no");
session.setConfig("StrictHostKeyChecking", "no");
}
int timeout = config.getConnectTimeout();
LOG.debug("Connecting to {} with {} timeout...", config.remoteServerInformation(), timeout > 0 ? (Integer.toString(timeout) + " ms") : "no");
if (timeout > 0) {
session.connect(timeout);
} else {
session.connect();
}
} catch (JSchException e) {
session = null;
LOG.warn("Could not create ssh session for " + config.remoteServerInformation(), e);
}
return session;
}
private void write(ChannelExec c, String name, InputStream data, ScpConfiguration cfg) throws IOException {
OutputStream os = c.getOutputStream();
InputStream is = c.getInputStream();
try {
writeFile(name, data, os, is, cfg);
} finally {
IOHelper.close(is, os);
}
}
private void writeFile(String filename, InputStream data, OutputStream os, InputStream is, ScpConfiguration cfg) throws IOException {
final int lineFeed = '\n';
String bytes;
int pos = filename.indexOf('/');
if (pos >= 0) {
// write to child directory
String dir = filename.substring(0, pos);
bytes = "D0775 0 " + dir;
LOG.trace("[scp:sink] {}", bytes);
os.write(bytes.getBytes());
os.write(lineFeed);
os.flush();
readAck(is, false);
writeFile(filename.substring(pos + 1), data, os, is, cfg);
bytes = "E";
LOG.trace("[scp:sink] {}", bytes);
os.write(bytes.getBytes());
os.write(lineFeed);
os.flush();
readAck(is, false);
} else {
int count = 0;
int read;
int size = endpoint.getBufferSize();
byte[] reply = new byte[size];
// figure out the stream size as we need to pass it in the header
BufferedInputStream buffer = new BufferedInputStream(data, size);
try {
buffer.mark(Integer.MAX_VALUE);
while ((read = buffer.read(reply)) != -1) {
count += read;
}
// send the header
bytes = "C0" + cfg.getChmod() + " " + count + " " + filename;
LOG.trace("[scp:sink] {}", bytes);
os.write(bytes.getBytes());
os.write(lineFeed);
os.flush();
readAck(is, false);
// now send the stream
buffer.reset();
while ((read = buffer.read(reply)) != -1) {
os.write(reply, 0, read);
}
writeAck(os);
readAck(is, false);
} finally {
IOHelper.close(buffer);
}
}
}
private void writeAck(OutputStream os) throws IOException {
os.write(0);
os.flush();
}
private int readAck(InputStream is, boolean failOnEof) throws IOException {
String message;
int answer = is.read();
switch (answer) {
case -1:
if (failOnEof) {
message = "[scp] Unexpected end of stream";
throw new EOFException(message);
}
break;
case 1:
message = "[scp] WARN " + readLine(is);
LOG.warn(message);
break;
case 2:
message = "[scp] NACK " + readLine(is);
throw new IOException(message);
default:
// case 0:
break;
}
return answer;
}
@SuppressWarnings("resource")
private String readLine(InputStream is) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
int c;
do {
c = is.read();
if (c == '\n') {
return bytes.toString();
}
bytes.write(c);
} while (c != -1);
} finally {
IOHelper.close(bytes);
}
String message = "[scp] Unexpected end of stream";
throw new IOException(message);
}
private static String getRemoteTarget(ScpConfiguration config) {
// use current dir (".") if target directory not specified in uri
return config.getDirectory().isEmpty() ? "." : config.getDirectory();
}
private static String getRemoteFile(String name, ScpConfiguration config) {
String dir = config.getDirectory();
dir = dir.endsWith("/") ? dir : dir + "/";
return name.startsWith(dir) ? name.substring(dir.length()) : name;
}
private static boolean isRecursiveScp(String name) {
return name.indexOf('/') > 0;
}
private static String getScpCommand(ScpConfiguration config, String name) {
StringBuilder cmd = new StringBuilder();
cmd.append("scp ");
// TODO: need config for scp *-p* (preserves modification times, access times, and modes from the original file)
// String command="scp " + (ptimestamp ? "-p " : "") + "-t " + configuration.getDirectory();
// TODO: refactor to use generic command
cmd.append(isRecursiveScp(name) ? "-r " : "");
cmd.append("-t ");
cmd.append(getRemoteTarget(config));
return cmd.toString();
}
protected static final class SessionUserInfo implements UserInfo, UIKeyboardInteractive {
private final ScpConfiguration config;
public SessionUserInfo(ScpConfiguration config) {
ObjectHelper.notNull(config, "config");
this.config = config;
}
@Override
public String getPassphrase() {
LOG.warn("Private Key authentication not supported");
return null;
}
@Override
public String getPassword() {
LOG.debug("Providing password for ssh authentication of user '{}'", config.getUsername());
return config.getPassword();
}
@Override
public boolean promptPassword(String message) {
LOG.debug(message);
return true;
}
@Override
public boolean promptPassphrase(String message) {
LOG.debug(message);
return true;
}
@Override
public boolean promptYesNo(String message) {
LOG.debug(message);
return false;
}
@Override
public void showMessage(String message) {
LOG.debug(message);
}
@Override
public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt, boolean[] echo) {
LOG.debug(instruction);
// Called for either SSH_MSG_USERAUTH_INFO_REQUEST or SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
// The most secure choice (especially for the second case) is to return null
return null;
}
}
}